// Copyright (c) 2020 Presto Labs Pte. Ltd.
// Author: jingyuan
#include "cc/appcoin2/support/account_control/approach_target_position_strategy.h"
#include "coin2/exchange/base/order/order.h"


typedef coin2::exchange::base::order::OrderContext OrderContext;

namespace coin2::appcoin2::support::account_control {

std::string GetProductKey(const IProduct* product) {
  return fmt::format("{}.{}", product->absolute_norm(), product->exchange_name());
}

void PaceControl::UpdateFill(int64_t timestamp, double fill_qty) {
  if (timestamp_begin == 0) timestamp_begin = timestamp;
  cum_fill_qty += fill_qty;
}

double PaceControl::GetRemainingPace(int64_t time_elapsed, double current_fill_qty) const {
  double rounds_cnt = (time_elapsed / 60e9) / (round_duration_minute);
  return rounds_cnt * lot_size0 - current_fill_qty;
}

double PaceControl::GetAggLotSize(int64_t time_elapsed, double current_fill_qty) const {
  double remain_pace_qty = GetRemainingPace(time_elapsed, current_fill_qty);
  if (remain_pace_qty > lot_size0) {
    return lot_size0;
  } else {
    return 0;
  }
}

void PaceControl::ExecuteOrder(
    int64_t timestamp,
    PassiveOrderExecutor* executor,
    double ask0,
    double bid0,
    double sell_price,
    double buy_price,
    double pass_bp,
    bool log_order,
    double initial_position,
    double target_position) {
  if (timestamp_begin == 0) timestamp_begin = timestamp;
  double time_elapsed = timestamp - timestamp_begin;
  double current_fill_qty = std::abs(target_position - initial_position) -
                            std::abs(target_position - executor->GetPosition());
  double lot_size_agg = GetAggLotSize(time_elapsed, current_fill_qty);

  if (lot_size_agg > 0 && round_hit_taker) {
    if (log_order) {
      LOG(INFO) << fmt::format(
          "time elapsed {}, current fill qty {}, lot size {}",
          time_elapsed,
          current_fill_qty,
          lot_size_agg);
    }
    double buy_price_agg = ask0 * (1 + taker_payup_bp * 1e-4);
    double sell_price_agg = bid0 * (1 - taker_payup_bp * 1e-4);
    bool taker_not_in_cooldown = ((timestamp - timestamp_last_order) / 1e9) > taker_cooldown_second;
    timestamp_last_order = timestamp;
    // executor will take care of position limits
    if (executor->RiskCheckBuy(lot_size_agg) == 0 &&
        executor->SubmitBuyOrder(timestamp, buy_price_agg, lot_size_agg) == 0 &&
        taker_not_in_cooldown) {
      LOG(INFO) << fmt::format(
          "taker buy {} @ {}, ask0 {} bid0 {}",
          lot_size_agg,
          buy_price_agg,
          ask0,
          bid0);

      executor->ForceFireBuyOrder(timestamp, buy_price_agg, lot_size_agg);
    }
    if (executor->RiskCheckSell(lot_size_agg) == 0 &&
        executor->SubmitSellOrder(timestamp, sell_price_agg, lot_size_agg) == 0 &&
        taker_not_in_cooldown) {
      LOG(INFO) << fmt::format(
          "taker sell {} @ {}, ask0 {} bid0 {}",
          lot_size_agg,
          sell_price_agg,
          ask0,
          bid0);
      executor->ForceFireSellOrder(timestamp, sell_price_agg, lot_size_agg);
    }
  }
  executor->ManageMmOrders(timestamp, sell_price, buy_price, 0, 0, false, false);
}

void ApproachTargetPositionStrategy::Init(Driver* driver) {
  driver_ = driver;
  auto get_product_holder =
      [this](const coin2::exchange::base::symbology::IProduct& product, int64_t timestamp) {
        return this->driver_->product_holder_manager()->GetProductHolder(product, timestamp);
      };

  auto driverConfig = driver->config();
  CHECK_EQ(driverConfig.order().exchanges().size(), 1);
  CHECK_EQ(driverConfig.order().exchanges().cbegin()->second.products().norms().size(), 1);
  auto symbol_str = driverConfig.order().exchanges().cbegin()->second.products().norms()[0];
  exe_config_.set_mea(driverConfig.order().exchanges().cbegin()->second.mea());
  exe_config_.set_symbol(symbol_str);
  exe_config_.set_lot_size(lot_size_);
  exe_config_.set_max_pos(target_position_);
  exe_config_.set_min_pos(target_position_);
  exe_config_.set_order_update_period(order_update_period_);
  exe_config_.set_max_posting_period(max_posting_period_);
  LOG(INFO) << fmt::format(
      "order alive for more than {} sec will be cancelled and replaced",
      max_posting_period_ / 1e9);
  exe_config_.set_sticky_bp(sticky_bp_);
  exe_config_.set_rounding_policy(coin2::strategy::TickRounding::TICK_ROUND_INWARD);
  exe_config_.set_price_push_bp(sticky_bp_);
  exe_config_.set_price_pull_bp(sticky_bp_);
  exe_config_.set_min_lot_size_ratio(0);
  exe_config_.set_fill_cooldown_period(fill_cooldown_window_);
  exe_config_.set_market_share_limit_window_period(market_share_limit_window_period_);
  exe_config_.set_market_share_limit(market_share_limit_);
  if (leverage_ != 0) {
    exe_config_.set_leverage(leverage_);
  }
  if (target_position_ == 0) {
    exe_config_.set_reduce_only(true);
    exe_config_.set_reserve(0);
  }
  exe_config_.set_close_only(hard_close_);

  auto mea = MarketExchangeApi::FromString(driverConfig.order().exchanges().cbegin()->second.mea());
  executor_ = std::make_unique<PassiveOrderExecutor>(
      exe_config_,
      driver->order()->GetUniqueSystem(mea),
      std::nullopt,
      nullptr,
      get_product_holder);

  int64_t ts = GetCurrentTimestamp();
  auto product = CreateProductFromNormString(mea, symbol_str, ts);
  product_key_ = GetProductKey(product.get());
  product_holder_ = driver->product_holder_manager()->GetProductHolder(*product, ts);
  if (tick_qty_ < 0) {
    tick_qty_ = product_holder_->product_info().tick_qty().submittable_min_qty();
    LOG(INFO) << "Using OG tick qty: " << tick_qty_;
  } else {
    LOG(INFO) << "Using specified tick qty: " << tick_qty_;
  }

  InitAuthKey(driver);
  strat_reporter_.reset(new StrategyReporter2(driver->strategy()->config()));
  coin::proto::AccountRequestProto acc_request;
  acc_request.set_market_type(MarketType_Name(mea.market));
  acc_request.set_exchange(ExchangeType_Name(mea.exchange));
  acc_request.set_api_version(mea.api);
  acc_request.set_owner(auth_key_->owner());
  strat_reporter_->RegisterExecutorConfig(ts, acc_request, *product, exe_config_);
}

void ApproachTargetPositionStrategy::onCleanup(Driver* /*driver*/) {
  if (executor_) {
    executor_->ForceCancelAllWorkingOrders();
    LOG(INFO) << "Cancel All Working Orders";
    LOG(INFO) << fmt::format(
        "cumulative fill qty {}, notional {}, vwap {}",
        filled_qty_,
        filled_notional_,
        std::abs(filled_qty_) > 1e-7 ? filled_notional_ / filled_qty_ : 0);
  }
}

void ApproachTargetPositionStrategy::onBookFeed(const FeedUpdate& upd) {
  if (!os()->is_ready()) {
    return;
  }
  CheckDone(upd.timestamp());
  if (!upd.book().Ask0().has_value() || !upd.book().Bid0().has_value()) {
    LOG(INFO) << fmt::format(
        "bid{} ask{} has no value, skipping",
        upd.book().Bid0().has_value(),
        upd.book().Ask0().has_value());
    return;
  }

  double ask0 = upd.book().Ask0()->price;
  double bid0 = upd.book().Bid0()->price;
  double mid = (ask0 + bid0) / 2;
  midp_1h_.Update(upd.timestamp(), mid);

  if (ignore_self_order_) {
    // Exclude the self order from the previous order
    const OrderContext* prevOrder = nullptr;
    double diff = mid;
    const auto& orders = os()->order_manager()->GetAllWorkingOrders();
    for (const auto& order : orders) {
      if (std::abs(order->order_price() - mid) < diff) {
        prevOrder = order;
        diff = std::abs(order->order_price() - mid);
      }
    } 
    double prev_order_price = 0.0;
    double prev_order_qty = 0.0;
    if (prevOrder != nullptr) {
      prev_order_price = prevOrder->order_price();
      prev_order_qty = prevOrder->order_qty() - prevOrder->filled_qty();
    }

    double ask0_qty = upd.book().Ask0()->qty;
    double bid0_qty = upd.book().Bid0()->qty;
    if (std::abs(ask0 - prev_order_price) < product_holder_->product_info().tick_price().UpTicksize(prev_order_price) && std::abs(ask0_qty - prev_order_qty) < tick_qty_) {
      if (upd.book().AskN(1).has_value()) {
        ask0 = upd.book().AskN(1)->price;
      }
    }
    if (std::abs(bid0 - prev_order_price) < product_holder_->product_info().tick_price().DownTicksize(prev_order_price) && std::abs(bid0_qty - prev_order_qty) < tick_qty_) {
      if (upd.book().BidN(1).has_value()) {
        bid0 = upd.book().BidN(1)->price;
      }
    }
  }

  double sell_price = NAN;
  double buy_price = NAN;
  mid = (ask0 + bid0) / 2;
  if (first_feed_ts_ == 0) {
    first_feed_ts_ = upd.timestamp();
  }
  if (first_feed_ts_ + 30'000'000'000L > upd.timestamp()) {
    return;
  }

  if (post_at_mid_) {
    sell_price =
        product_holder_->product_info().tick_price().GetNextPrice((1 + pass_bp_ / 1e4) * mid);
    buy_price =
        product_holder_->product_info().tick_price().GetPrevPrice((1 - pass_bp_ / 1e4) * mid);
  } else {
    sell_price = (1 + pass_bp_ / 1e4) * ask0;
    buy_price = (1 - pass_bp_ / 1e4) * bid0;
  }

  coin2::strategy::TickRounding rounding_policy_;
  coin2::strategy::TickRounding_Parse(rounding_policy_str_, &rounding_policy_);
  if (sell_price > 0) {
    sell_price = product_holder_->product_info().tick_price().RoundWithPolicy(
        sell_price,
        OrderDirection::DIRECTION_SELL,
        rounding_policy_);
  }

  if (buy_price > 0) {
    buy_price = product_holder_->product_info().tick_price().RoundWithPolicy(
        buy_price,
        OrderDirection::DIRECTION_BUY,
        rounding_policy_);
  }

  bool log_order = upd.timestamp() - last_report_ts_ > 1'000'000'000LL;

  if (max_price_ > 0) {
    buy_price = std::min(buy_price, max_price_);
  }
  if (min_price_ > 0) {
    sell_price = std::max(sell_price, min_price_);
  }
  if (buy_price >= ask0) {
    buy_price = product_holder_->product_info().tick_price().GetPrevPrice(ask0);
  }
  if (sell_price <= bid0) {
    sell_price = product_holder_->product_info().tick_price().GetNextPrice(bid0);
  }

  double ma1h_price = midp_1h_.average();
  double ma1h_buy_cap = ma1h_price * (1 + 1e-4 * limit_payup_bp_to_ma1h_);
  double ma1h_sell_floor = ma1h_price * (1 - 1e-4 * limit_payup_bp_to_ma1h_);

  buy_price = std::min(buy_price, ma1h_buy_cap);
  sell_price = std::max(sell_price, ma1h_sell_floor);
  if (log_order) {
    LogPosition();
    executor_->LogStatus();
    LOG(INFO) << fmt::format(
        "ask0 {} bid0 {} sell_price {} buy_price {} pass_bp_ {} ma1h_buy_cap {} ma1h_sell_flr {}",
        ask0,
        bid0,
        sell_price,
        buy_price,
        pass_bp_,
        ma1h_buy_cap,
        ma1h_sell_floor);
    last_report_ts_ = upd.timestamp();
  }
  if (!initial_position_.has_value()) {
    CHECK(initial_position_) << "initial_position must have value";
  }
  double initial_position = initial_position_.value();
  pace_control_.ExecuteOrder(
      upd.timestamp(),
      executor_.get(),
      ask0,
      bid0,
      sell_price,
      buy_price,
      pass_bp_,
      log_order,
      initial_position,
      target_position_);
  if (strat_reporter_) {
    if (GetProductKey(&upd.product()) == product_key_) {
      strat_reporter_->UpdateMidp(upd.product(), mid);
    }
  }
}

void ApproachTargetPositionStrategy::onAccountOrder(const OrderUpdate& upd) {
  if (upd.product_order_info().event().type() == coin::proto::OrderEvent::ORDER_FILLED &&
      GetProductKey(&upd.product()) == product_key_) {
    double qty = upd.product_order_info().event().fill_qty();
    double price = upd.product_order_info().event().fill_price();
    filled_qty_ += qty;
    filled_notional_ += price * qty;
    LOG(INFO) << fmt::format(
        "Order Filled:{} time {} side {} price {} qty {}, cum qty {}, cum notional {}",
        executor_->product().order_native_symbol(),
        upd.product_order_info().event().event_time(),
        upd.product_order_info().event().trade_side(),
        price,
        qty,
        filled_qty_,
        filled_notional_);
    if (upd.product_order_info().event().trade_side() == coin::proto::TRADE_SELL_SIDE) {
      qty = -qty;
    }
    pace_control_.UpdateFill(upd.timestamp(), qty);
    executor_->onAccountOrder(upd);
    CheckDone(upd.timestamp());
    UpdateMinMaxPosition(upd.timestamp());
  }
}

void ApproachTargetPositionStrategy::onOrderLog(const coin::proto::OrderGatewayLog& order_log) {
  if (strat_reporter_) {
    strat_reporter_->GetMutableStrategyLogger()->WriteOrderLog(
        order_log,
        true,
        GetCurrentTimestamp());
  }
}

void ApproachTargetPositionStrategy::InitAuthKey(::coin2::app::Driver* driver) {
  const auto& driver_config = driver->config();
  for (const auto& [exchange, order_config] : driver_config.order().exchanges()) {
    CHECK(!auth_key_);
    const auto& key_filepath = order_config.connection_config().key_filepath();
    auth_key_.reset(new AuthKey(AuthKey::FromFile(key_filepath)));
  }
}

void ApproachTargetPositionStrategy::onTradeFeed(const FeedUpdate& upd) {
  executor_->onTradeFeed(upd);
}

void ApproachTargetPositionStrategy::onAccountInfo(const OrderUpdate& upd) {
  const auto& info = upd.account_info();
  if (!os()->is_ready() || !info.is_ready()) {
    return;
  }
  if (strat_reporter_) {
    strat_reporter_->onAccountInfo(upd);
  }
  if (upd.timestamp() - last_position_ts_ > 5'000'000'000LL) {
    LOG(INFO) << os()->gateway()->order_manager().DebugString(upd.timestamp());
    last_position_ts_ = upd.timestamp();
    nlohmann::json j;
    auto& balances = j["balances"];
    for (const auto& [k, v] : info.GetBalances()) {
      balances[k] = v.total();
    }
    auto& positions = j["positions"];
    for (const auto& [k, v] : info.GetPositions()) {
      positions[k]["net"] = v.net_position();
      positions[k]["long"] = v.available_long_position();
      positions[k]["short"] = v.available_short_position();
    }

    LOG(INFO) << "[" << std::setw(7) << MarketType_Name(upd.mea().market) << "," << std::setw(8)
              << ExchangeType_Name(upd.mea().exchange) << "] "
              << Iso8601FromTimestamp(upd.timestamp()) << "(" << j.dump(2) << ")" << std::endl;
  }
  executor_->onAccountInfo(upd);
  CheckDone(upd.timestamp());
  UpdateMinMaxPosition(upd.timestamp());
}

void ApproachTargetPositionStrategy::CheckDone(int64_t timestamp) {
  if (!os()->is_ready()) {
    return;
  }
  bool done = false;
  int exit_code = 0;
  if (hard_close_) {
    if (!initial_position_.has_value()) {
      initial_position_ = executor_->GetTotalLongPosition() - executor_->GetTotalShortPosition();
      shutdown_ts_ += timestamp;
      LOG(INFO) << "Auto shutdown at " << shutdown_ts_;
    }
    if (isQtyClose(executor_->GetTotalLongPosition(), 0) &&
        isQtyClose(executor_->GetTotalShortPosition(), 0)) {
      LOG(INFO) << fmt::format(
          "Cancelling all orders as position is closed Long Pos {} Short Pos {} tick_size {}",
          executor_->GetTotalLongPosition(),
          executor_->GetTotalShortPosition(),
          tick_qty_);
      done = true;
    }
  } else {
    if (!initial_position_.has_value()) {
      initial_position_ = executor_->GetPosition();
      shutdown_ts_ += timestamp;
      if (use_target_position_as_delta_) {
        LOG(INFO) << fmt::format(
            "Update target position {} to {}",
            target_position_,
            target_position_ + executor_->GetPosition());
        target_position_ += executor_->GetPosition();
      }
      LOG(INFO) << "Auto shutdown at " << shutdown_ts_;
    }
    if (isQtyClose(executor_->GetPosition(), target_position_)) {
      LOG(INFO) << fmt::format(
          "Cancelling all orders as position is reached now {} vs target {} tick_size {}",
          executor_->GetPosition(),
          target_position_,
          tick_qty_);
      done = true;
    }
  }

  if (timestamp > shutdown_ts_) {
    LOG(INFO) << fmt::format("shutdown from timer {} now {}", shutdown_ts_, timestamp);
    done = true;
    exit_code = 1;
  }

  if (done) {
    onCleanup(nullptr);
    if (os()->gateway()->order_manager().GetAllWorkingOrders().empty()) {
      double total_filled = 0;
      if (hard_close_) {
        total_filled = initial_position_.value();
      } else {
        total_filled = std::abs(initial_position_.value() - executor_->GetPosition());
      }
      LOG(INFO) << fmt::format(
          "No working orders, exit. Total fill Qty from config {}",
          total_filled);
      std::exit(exit_code);
    }
  }
}

void ApproachTargetPositionStrategy::UpdateMinMaxPosition(int64_t timestamp) {
  if (hard_close_) {
    executor_->UpdateMinMaxPosition(
        executor_->GetTotalShortPosition(),
        executor_->GetTotalLongPosition(),
        timestamp);
  } else {
    executor_->UpdateMinMaxPosition(
        std::min(executor_->GetPosition(), target_position_),
        std::max(executor_->GetPosition(), target_position_),
        timestamp);
  }
}

void ApproachTargetPositionStrategy::LogPosition() {
  if (hard_close_) {
    LOG(INFO) << fmt::format(
        "{} Long Pos {} Short Pos {} target_position_{} tick_qty {}",
        driver_->config().order().exchanges().cbegin()->second.products().norms()[0],
        executor_->GetTotalLongPosition(),
        executor_->GetTotalShortPosition(),
        target_position_,
        tick_qty_);
  } else {
    LOG(INFO) << fmt::format(
        "{} Now {} vs target{} tick_qty {}",
        driver_->config().order().exchanges().cbegin()->second.products().norms()[0],
        executor_->GetPosition(),
        target_position_,
        tick_qty_);
  }
}

IOrderSubsystem* ApproachTargetPositionStrategy::os() {
  auto mea =
      MarketExchangeApi::FromString(driver_->config().order().exchanges().cbegin()->second.mea());
  return driver_->order()->GetUniqueSystem(mea);
}

bool ApproachTargetPositionStrategy::isQtyClose(double a, double b) {
  return std::abs(a - b) < tick_qty_;
}

}  // namespace coin2::appcoin2::support::account_control
